/*
 * Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.imageio.stream;

import java.io.File;
import java.io.InputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import com.sun.imageio.stream.StreamCloser;
import com.sun.imageio.stream.StreamFinalizer;
import sun.java2d.Disposer;
import sun.java2d.DisposerRecord;

/**
 * An implementation of <code>ImageInputStream</code> that gets its
 * input from a regular <code>InputStream</code>.  A file is used to
 * cache previously read data.
 */
public class FileCacheImageInputStream extends ImageInputStreamImpl {

  private InputStream stream;

  private File cacheFile;

  private RandomAccessFile cache;

  private static final int BUFFER_LENGTH = 1024;

  private byte[] buf = new byte[BUFFER_LENGTH];

  private long length = 0L;

  private boolean foundEOF = false;

  /**
   * The referent to be registered with the Disposer.
   */
  private final Object disposerReferent;

  /**
   * The DisposerRecord that closes the underlying cache.
   */
  private final DisposerRecord disposerRecord;

  /**
   * The CloseAction that closes the stream in
   * the StreamCloser's shutdown hook
   */
  private final StreamCloser.CloseAction closeAction;

  /**
   * Constructs a <code>FileCacheImageInputStream</code> that will read
   * from a given <code>InputStream</code>.
   *
   * <p> A temporary file is used as a cache.  If
   * <code>cacheDir</code>is non-<code>null</code> and is a
   * directory, the file will be created there.  If it is
   * <code>null</code>, the system-dependent default temporary-file
   * directory will be used (see the documentation for
   * <code>File.createTempFile</code> for details).
   *
   * @param stream an <code>InputStream</code> to read from.
   * @param cacheDir a <code>File</code> indicating where the cache file should be created, or
   * <code>null</code> to use the system directory.
   * @throws IllegalArgumentException if <code>stream</code> is <code>null</code>.
   * @throws IllegalArgumentException if <code>cacheDir</code> is non-<code>null</code> but is not a
   * directory.
   * @throws IOException if a cache file cannot be created.
   */
  public FileCacheImageInputStream(InputStream stream, File cacheDir)
      throws IOException {
    if (stream == null) {
      throw new IllegalArgumentException("stream == null!");
    }
    if ((cacheDir != null) && !(cacheDir.isDirectory())) {
      throw new IllegalArgumentException("Not a directory!");
    }
    this.stream = stream;
    if (cacheDir == null) {
      this.cacheFile = Files.createTempFile("imageio", ".tmp").toFile();
    } else {
      this.cacheFile = Files.createTempFile(cacheDir.toPath(), "imageio", ".tmp")
          .toFile();
    }
    this.cache = new RandomAccessFile(cacheFile, "rw");

    this.closeAction = StreamCloser.createCloseAction(this);
    StreamCloser.addToQueue(closeAction);

    disposerRecord = new StreamDisposerRecord(cacheFile, cache);
    if (getClass() == FileCacheImageInputStream.class) {
      disposerReferent = new Object();
      Disposer.addRecord(disposerReferent, disposerRecord);
    } else {
      disposerReferent = new StreamFinalizer(this);
    }
  }

  /**
   * Ensures that at least <code>pos</code> bytes are cached,
   * or the end of the source is reached.  The return value
   * is equal to the smaller of <code>pos</code> and the
   * length of the source file.
   */
  private long readUntil(long pos) throws IOException {
    // We've already got enough data cached
    if (pos < length) {
      return pos;
    }
    // pos >= length but length isn't getting any bigger, so return it
    if (foundEOF) {
      return length;
    }

    long len = pos - length;
    cache.seek(length);
    while (len > 0) {
      // Copy a buffer's worth of data from the source to the cache
      // BUFFER_LENGTH will always fit into an int so this is safe
      int nbytes =
          stream.read(buf, 0, (int) Math.min(len, (long) BUFFER_LENGTH));
      if (nbytes == -1) {
        foundEOF = true;
        return length;
      }

      cache.write(buf, 0, nbytes);
      len -= nbytes;
      length += nbytes;
    }

    return pos;
  }

  public int read() throws IOException {
    checkClosed();
    bitOffset = 0;
    long next = streamPos + 1;
    long pos = readUntil(next);
    if (pos >= next) {
      cache.seek(streamPos++);
      return cache.read();
    } else {
      return -1;
    }
  }

  public int read(byte[] b, int off, int len) throws IOException {
    checkClosed();

    if (b == null) {
      throw new NullPointerException("b == null!");
    }
    // Fix 4430357 - if off + len < 0, overflow occurred
    if (off < 0 || len < 0 || off + len > b.length || off + len < 0) {
      throw new IndexOutOfBoundsException
          ("off < 0 || len < 0 || off+len > b.length || off+len < 0!");
    }

    bitOffset = 0;

    if (len == 0) {
      return 0;
    }

    long pos = readUntil(streamPos + len);

    // len will always fit into an int so this is safe
    len = (int) Math.min((long) len, pos - streamPos);
    if (len > 0) {
      cache.seek(streamPos);
      cache.readFully(b, off, len);
      streamPos += len;
      return len;
    } else {
      return -1;
    }
  }

  /**
   * Returns <code>true</code> since this
   * <code>ImageInputStream</code> caches data in order to allow
   * seeking backwards.
   *
   * @return <code>true</code>.
   * @see #isCachedMemory
   * @see #isCachedFile
   */
  public boolean isCached() {
    return true;
  }

  /**
   * Returns <code>true</code> since this
   * <code>ImageInputStream</code> maintains a file cache.
   *
   * @return <code>true</code>.
   * @see #isCached
   * @see #isCachedMemory
   */
  public boolean isCachedFile() {
    return true;
  }

  /**
   * Returns <code>false</code> since this
   * <code>ImageInputStream</code> does not maintain a main memory
   * cache.
   *
   * @return <code>false</code>.
   * @see #isCached
   * @see #isCachedFile
   */
  public boolean isCachedMemory() {
    return false;
  }

  /**
   * Closes this <code>FileCacheImageInputStream</code>, closing
   * and removing the cache file.  The source <code>InputStream</code>
   * is not closed.
   *
   * @throws IOException if an error occurs.
   */
  public void close() throws IOException {
    super.close();
    disposerRecord.dispose(); // this will close/delete the cache file
    stream = null;
    cache = null;
    cacheFile = null;
    StreamCloser.removeFromQueue(closeAction);
  }

  /**
   * {@inheritDoc}
   */
  protected void finalize() throws Throwable {
    // Empty finalizer: for performance reasons we instead use the
    // Disposer mechanism for ensuring that the underlying
    // RandomAccessFile is closed/deleted prior to garbage collection
  }

  private static class StreamDisposerRecord implements DisposerRecord {

    private File cacheFile;
    private RandomAccessFile cache;

    public StreamDisposerRecord(File cacheFile, RandomAccessFile cache) {
      this.cacheFile = cacheFile;
      this.cache = cache;
    }

    public synchronized void dispose() {
      if (cache != null) {
        try {
          cache.close();
        } catch (IOException e) {
        } finally {
          cache = null;
        }
      }
      if (cacheFile != null) {
        cacheFile.delete();
        cacheFile = null;
      }
      // Note: Explicit removal of the stream from the StreamCloser
      // queue is not mandatory in this case, as it will be removed
      // automatically by GC shortly after this method is called.
    }
  }
}
